Error Handling

我们可以将错误处理配置拆解为三个核心部分:监控层 (onError/onCatch)展示层 (errorComponent)恢复机制

1. 监控层:onErroronCatch

这两个属性主要用于“逻辑副作用”,即在错误发生时执行代码(如记录日志),而不是渲染 UI。

2. 展示层:errorComponent

这是最重要的配置,它决定了当错误发生时,用户在界面上看到的内容。

3. 实现“点击重试”功能

利用 errorComponent 提供的 reset 参数,你可以让用户在网络波动失败后一键恢复,而不需要刷新整个网页。

老师案例

image-20251226213015742

可以看到,errorComponent展示了,但是点击Try again按钮没有发起重新请求,这是因为reset并不会发起loader里面的请求。

可以使用router.inValidate()来刷新路由。

image-20251226214014854

可以看到,点击重试按钮之后,重新发起了请求。这对于因为网络造成的页面错误是有用的。

为什么 reset() 不 refetch loader?reset() 的作用是:

但你的 loader 是在路由加载阶段(load phase)执行的:

当错误发生在 loader 里时:

社区常见 issue(如 #2539)已经确认了这一点:

If an error occurs in beforeLoad/loader/... then calling reset() will not try to run the route again.

Not Found Errors

在 TanStack Router 中,notFound(404 错误)的处理主要分为两个维度:全局配置组件内手动触发

与一般的 errorComponent 不同,notFound 专门用于处理“路径匹配成功但资源不存在”或“路径完全无法匹配”的情况。

1. 处理“路径无法匹配” (全局 404)

当用户访问了一个在路由树(Route Tree)中根本不存在的 URL 时,会触发全局的 NotFound 逻辑。

你需要在 createRouter 中配置 defaultNotFoundComponent

2. 处理“资源不存在” (手动触发 404)

这是最常见的场景:用户访问 /posts/123,路由匹配成功了,但数据库里没有 ID 为 123 的文章。此时你应该在 loader 或组件中手动抛出 notFound()

在 Loader 中触发:

3. notFounderrorComponent 的区别

很多开发者会混淆这两者,它们的触发逻辑完全不同:

特性notFoundComponenterrorComponent
触发原因手动调用 notFound() 或路径完全不匹配代码崩溃、API 报错(500/网络超时等)
语义"资源没找到" (404)"程序出错了" (5xx/Runtime Error)
默认行为寻找最近的 notFoundComponent寻找最近的 errorComponent

4. 嵌套路由中的 NotFound

TanStack Router 支持 局部 404

image-20251226220133822

 

image-20251226220017217

5. 常见配置项汇总

除了组件本身,你还可以在路由定义中配置:

总结建议

  1. 必须设置:在 createRouter 里给个 defaultNotFoundComponent 兜底。虽然tanstack为我们做了默认的notFound功能,但最好还是自己做一个样式合适的页面。
  2. 按需设置:在详情页(如用户、商品、文章)的路由里定义具体的 notFoundComponent,提供更精准的引导(比如“换个关键词搜搜”)。

CatchBoundary

在 TanStack Router 中,CatchBoundary 是一个专门用于局部捕获并处理渲染错误的底层组件。虽然你已经在路由配置中看到了 errorComponent,但 CatchBoundary 提供了更细粒度的控制,尤其是在处理非路由级别的组件崩溃时。

1. 核心定义

CatchBoundary 本质上是 React 错误边界(Error Boundary)的一个封装。它的作用是:当其子组件在渲染过程中抛出错误时,捕获该错误并展示一个“降级 UI”,而不是让整个路由或整个页面崩溃。

2. 与 errorComponent 的区别

这是最容易混淆的地方,两者的分工如下:

特性errorComponent (路由级)CatchBoundary (组件级)
触发点loader 失败或路由主组件崩溃任何被它包裹的子组件崩溃
控制粒度替换整个路由出口(Outlet)只替换被包裹的局部区域
使用场景处理页面级数据加载错误处理复杂的局部 UI 组件(如第三方图表)崩溃

3. 如何使用 CatchBoundary

你可以在任何组件树中使用它来包裹可能存在风险的代码块。

4. 关键属性说明

5. 为什么在 TanStack Router 中它很重要?

在现代 React 应用中,数据加载和 UI 渲染是高度耦合的。CatchBoundary 让你可以实现“容错性布局”

  1. Loader 错误:由路由的 errorComponent 处理。
  2. 渲染错误:由 CatchBoundary 处理。
  3. 404 错误:由 notFoundComponent 处理。

总结

CatchBoundary 是你的局部防火墙。如果你的页面里有一个非常复杂、容易报错的子组件(比如复杂的表格或第三方库渲染),用 CatchBoundary 把它围起来,就能保证即使它挂了,用户依然能操作页面的其他部分。

老师案例

假设在请求comments的时候抛出了错误,此时整个页面都会显示errorComponent的内容:

但是post部分是好的,我只想在comments部分出现错误时展示错误信息,此时就可以使用CatchBoundary来处理。

可以看到,只有comments部分显示报错内容,其余的部分都是好的,这很好啊。

Code Splitting

在 TanStack Router 中,Code Splitting(代码分割) 是提升首屏加载速度的核心技术。它的目标是:只加载当前页面所需的代码,而不是一次性下载整个应用的 JS 文件。

参考文档:https://tanstack.com/router/latest/docs/framework/react/guide/code-splitting

目前只需要关注下面这一点,自动分割即可,更加复杂的不要管了:

image-20251226222531136

Preloading

在 TanStack Router 中,Preloading(预加载) 是一项旨在消除用户感知延迟的技术。它的核心思路是:在用户真正点击链接之前,就提前加载好该路由所需的代码(JS 束)和数据(Loader 数据)

1. 预加载的触发时机 (Preload Strategies)

你可以通过 preload 属性控制什么时候开始预加载:

2. 预加载了什么内容?

当预加载触发时,Router 会并行执行以下两个任务:

  1. 代码预加载:下载该路由对应的 .lazy.tsx 组件包。
  2. 数据预加载:执行该路由的 loader 函数,并将结果存入缓存。

这样当用户最终点击时,由于代码和数据都已经准备就绪,页面可以实现瞬间切换,跳过全屏加载状态(Pending State)。

3. 如何配置 Preloading

全局配置

通常在 createRouter 中设置默认行为,这样全站的 <Link> 都会生效:

局部配置

你也可以在具体的 <Link> 组件上覆盖全局设置:

社区目前的主流做法是:

4. 预加载的“保鲜期”:preloadStaleTime

为了防止浪费流量(例如用户频繁 hover 多个链接),Router 使用了 preloadStaleTime

5. 手动触发预加载

有时候你需要通过代码手动触发预加载(例如在某个动画完成后),可以使用 router.preloadRoute

总结与对比

特性无 Preloading有 Preloading (intent)
点击瞬间开始下载 JS + 请求数据立即渲染组件
用户体验看到 Loading 界面秒开,无缝衔接
服务器压力较低略高(会有部分无效 hover 请求)

预加载是让单页应用(SPA)产生“原生应用感”的关键。

老师案例

可以看到,鼠标悬浮之后,里面的内容就会预加载。

Caching & Caching options

在 TanStack Router 中,缓存(Caching) 机制是其高性能的核心。它通过智能地管理数据的“新鲜度”和“生命周期”,确保应用在切换路由时能够瞬间响应,同时避免不必要的网络请求。

TanStack Router 的缓存逻辑深受 TanStack Query 的启发,主要围绕 staleTimegcTime 这两个核心概念展开。

1. 核心概念:数据的三种状态

2. 两个关键参数

createRouterdefaultOptions 中,你可以全局控制缓存的行为:

参数默认值作用说明
staleTime0 ms“保鲜期”。在此时间内,Router 认为数据是绝对可靠的,不会重新触发 loader
gcTime30 分钟“垃圾回收期”。当数据进入“不活跃”状态后,在内存中保留的时间。超过后会被销毁。

配置示例:

在 TanStack Router 中,staleTime 的默认值根据具体用途(路由数据缓存 vs. 预加载缓存)有所不同:


为什么路由缓存的默认值是 0?

TanStack Router 默认采用“积极验证”策略:

  1. 确保准确性:默认不缓存(0ms)可以保证用户每次进入页面看到的数据都是最新的。
  2. 触发机制:虽然 staleTime 为 0,但如果数据已经在内存中且请求正在进行,Router 会智能处理,不会导致重复的并发请求。
  3. 其实也更方便了,如果结合tanstack/query一起使用,就不用特别设置了。

3. 缓存如何与 Loader 协作?

当你点击一个链接时,TanStack Router 的缓存检查流程如下:

  1. 匹配路由:找到目标路由及其对应的 loader

  2. 检查缓存

    • 如果缓存中已有数据且处于 Fresh 状态:跳过 loader,直接渲染页面。
    • 如果数据处于 Stale 状态:执行 loader,但在数据回来前,如果有旧数据会尝试展示。
    • 如果没有缓存:执行 loader 并显示 pendingComponent(如果超时)。

4. 缓存失效与主动刷新

有时候你需要手动让缓存失效(例如提交表单后),可以通过以下方式:

5. 缓存与预加载(Preloading)的联动

缓存机制让 Preloading 变得非常有意义:

  1. 当用户 hover 链接时,触发预加载。
  2. 数据被存入缓存并标记为 Fresh
  3. 当用户正式点击时,Router 发现数据是 Fresh 的,实现瞬间秒开

总结建议

老师案例

/posts/$postId页面添加staleTime,看一下缓存:

可以看到,进入详情页之后,除了第一次请求了,后面的都是瞬间渲染,这就是缓存的作用。

 

问题:

如果你同时使用了 TanStack Query,通常建议将路由层的缓存逻辑交给 Query 处理,让 Router 的 loader 仅作为触发器。

 

这个怎么做?tanstack router不是自带有缓存吗?

确实,TanStack Router 自带缓存机制,但当你引入 TanStack Query (React Query) 时,你会发现两者的功能在“数据存储”和“缓存管理”上是有重叠的。

为了避免“一个数据存两份”以及缓存同步混乱,业界推荐的 “Router + Query 黄金搭档” 模式是:利用 Router 驱动跳转,利用 Query 管理数据。


1. 为什么要“舍弃” Router 缓存?

虽然 Router 能够缓存数据,但 TanStack Query 在数据管理上更专业:


2. 具体怎么做?(核心步骤)

第一步:在 loader 中仅作为“触发器”

不要在 loader 里直接 fetch 数据并返回,而是调用 QueryClient 的 ensureQueryData 方法。这个方法的作用是:“如果缓存里有新鲜的就直接给路由,没有就去下载并存入 Query 缓存。”

第二步:在组件中使用 useSuspenseQuery

在页面组件里,不要再用 Route.useLoaderData(),而是直接使用 Query 的 Hook。


3. 配置 Router 禁用自带缓存

为了让逻辑完全归于 Query,我们需要在 Router 层做一些“减法”,防止 Router 的缓存机制干扰 Query 的刷新逻辑。

createRouter 中,建议将 staleTime 设为 0


4. 这种模式下的数据流

  1. 用户点击链接:Router 匹配到路由。

  2. 执行 Loader:调用 queryClient.ensureQueryData

  3. Query 介入

    • Query 检查自己的缓存。
    • 如果有数据且未过期(Query 的 staleTime),直接返回。
    • 如果没有或已过期,发起请求,存入 Query 缓存,然后返回。
  4. 渲染组件:组件通过 useSuspenseQuery 拿到 Query 缓存中的数据。

总结建议

这样做之后,你的整个应用就拥有了“路由级触发”和“专业级缓存”的双重优势。

Protecting Routes

可以为一个路由单独做protect,也可以为多个路由一起做protect。都是使用 beforeLoad 属性。

在 TanStack Router 中,实现 Protect Route(受保护路由/权限路由) 的标准做法是使用 beforeLoad 属性。

beforeLoad 是路由生命周期中的第一个环节,它运行在 loader 之前。如果校验失败,你可以直接触发重定向或抛出错误,从而阻止用户进入该页面。

1. 核心逻辑实现

最常见的方法是在 RootRoute 或一个 Pathless Route(无路径路由) 中统一处理认证逻辑。

示例:创建一个受保护的布局路由

你可以创建一个名为 _authenticated.tsx 的文件(下划线开头表示不增加 URL 层级),让所有需要登录的页面都作为它的子路由。

2. 在 Router 中注入 Auth 上下文

为了让 beforeLoad 能访问到登录状态,你需要在创建 Router 时通过 context 注入。

3. 为什么选择 beforeLoad 而不是在组件内判断?

  1. 性能更优:在 JS 包下载完、Loader 执行前就完成了拦截,避免了不必要的 API 请求。
  2. 防止闪烁:如果进入组件后再判断,用户可能会看到一瞬间的受保护内容(Layout 闪烁),beforeLoad 能彻底杜绝这个问题。
  3. 类型安全:你可以通过 TypeScript 强制要求某些路由必须拥有 auth 上下文。

4. 处理更复杂的权限(角色权限)

如果你需要判断用户是否有特定的权限(如:只有管理员能看),逻辑是一样的:

5. 关于 redirect 的注意事项

总结

老师案例

这节课先学习为单个文件设置拦截。

假设我们跳转到/about页面,需要先检查是否登录了,如果没有登录,则跳转到/login页面。

1、为/about页面设置拦截

image-20251227052759031

2、创建login页面,处理登录逻辑

可以看到,访问 /about 如果没有登录,就跳转到 /login 页面,登录之后返回原页面。

 

问题:登录成功之后,返回 /about 页面,但是一刷新,就又跳转到 /login 页面了,context里面明明存了数据的啊?


核心原因在于:TanStack Router 的 context 是内存中的临时状态,页面刷新后全部丢失。

1、context.auth.user 的来源

这个 context.auth.user 是每次路由渲染时从 <RouterProvider context={{ auth }}> 注入的。

2、页面刷新时发生了什么?

3、一句话总结:刷新 = 内存清空 = 登录状态丢失 = 又被踢回登录页

你看到的“明明有数据”只是假象 在不刷新的情况下(点击登录按钮 → login() → setUser(...) → invalidate()),context 确实更新了,页面能正常跳转到 /about。但一刷新,setUser 做的所有状态就没了。

解决办法:

方案 1:最简单(开发/学习阶段) - 使用 localStorage

方案 2:更安全(生产环境) - 使用 HttpOnly cookie + token

  1. 登录时,后端返回 token,并设置 HttpOnly cookie
  2. 前端通过 API 请求(带 cookie)获取当前用户信息
  3. 在应用启动时(比如 App 根组件)调用一个 checkAuth API 来恢复 user

然后 beforeLoad 里检查的就是这个恢复后的 user。

Protecting Multiple Routes with Layouts

这节课学习为多个路由设置拦截。

之前学习过Pathless Layouts,无路由路径。意思就是:

image-20251227055104109

因此,我们可以将需要拦截的路由,全部放到这个路径下面,在这个文件里面编写beforeLoad拦截。

1、创建一个_authenticated.tsx文件

2、在_authenticated底下创建路由文件,这样这些路由都会得到拦截

类似的,还可以创建很多。

可以看到,_authenticated底下的路由,不用在自己的文件代码里面写beforeLoad了,而且都受到保护了。一次性搞定。

不直接跳转,让用户点击按钮跳转

有时候会觉得直接跳转的用户体验不好,可以先显示一些信息,然后用户点击按钮跳转到登录页面。

可以看到,不是直接跳转到login页面,而是先展示一些信息,让用户自己跳转到login页面。登录后返回原页面。

getRouteApi

getRouteApi 是 TanStack Router 中一个非常实用、类型安全的工具函数,它的主要作用是:让你在任意组件(甚至非路由组件)中,以强类型的方式访问特定路由的 API(包括 params、search、loader data、context 等),而不需要依赖 useParamsuseSearchuseLoaderData 等 hook。

简单来说,它相当于“路由专属的类型安全 hook 生成器”。

核心作用总结

getRouteApi(routeId) → 返回一个对象,里面包含一系列类型安全的 hook 和工具函数,这些 hook 只针对指定的路由有效。

它返回什么?(常用成员)

典型使用场景对比(为什么需要它)

场景描述不用 getRouteApi 的写法用 getRouteApi 的写法优势
在子组件里获取 posts 详情页的 postIdconst { postId } = Route.useParams()const { postId } = api.useParams()类型安全,IDE 提示精确
在非路由组件里用 loader 数据只能传 props,或者用全局 contextconst data = api.useLoaderData()直接拿,不用层层 props
组件复用在多个路由,但想针对不同路由取不同数据很难做类型安全每个路由用不同的 getRouteApi强类型隔离
写一个只在 /posts/$postId 下才渲染的组件写死 useParams 容易出错用 api.useParams(),编译期就知道必须有 postId防呆
在深层嵌套组件里访问父路由的 context层层 useRouteContext() + 类型断言api.useRouteContext() 直接拿干净 + 类型安全

真实代码例子(最常见的几种用法)

什么时候最应该用 getRouteApi?

简单记口诀:

“组件深了用 props 烦?用 getRouteApi 直接戳路由!”

More Topics & Outro

还有很多概念需要了解一下,比如说:Route Masking、Navigation Blocking、SSR、Type Utilities。